package com.hl.scriptDemo.controller;

import com.hl.scriptDemo.scripts.RuleScript;
import com.hl.scriptDemo.util.GroovyScriptCache;
import com.hl.scriptDemo.util.RedisUtils;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.groovy.control.CompilationFailedException;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;

/**
 * @author hulei
 * @date 2024/2/7 17:43
 */

@RestController
@RequestMapping("/scripts")
@Slf4j
public class RuleScriptController {
    private final RedisUtils redisUtils;
    private final GroovyScriptCache<GroovyObject> groovyScriptCache;

    public RuleScriptController(RedisUtils redisUtils, GroovyScriptCache<GroovyObject> groovyScriptCache) {
        this.redisUtils = redisUtils;
        this.groovyScriptCache = groovyScriptCache;
    }

    /**
     * 执行脚本，脚本实例化为类对象,此种方式不灵活，需要编写groovy类，前端传入类的字符串，然后编译成字节码，再实例化，这种方式不推荐
     * 有一点就是安全性，因为前端传入的字符串是用户输入的，可能存在恶意代码，所以编写groovy类是为了保证安全性
     * {@link com.hl.scriptDemo.scripts.RuleScript}
     */
    @PostMapping("/executeScriptByClass")
    public String executeScriptByClass(@RequestBody String scriptContent) throws CompilationFailedException, IOException {
        // 检查Redis中是否已存在相同脚本
        if (Boolean.TRUE.equals(redisUtils.hasKey(scriptContent))) {
            //直接从缓存中获取已实例化的Groovy对象并转换为对应的RuleScript对象
            RuleScript script = redisUtils.get(scriptContent);
            assert script != null;
            script.execute();
            redisUtils.delete(Collections.singleton(scriptContent));
            return "执行缓存中的脚本";
        } else {
            // 尝试从Redis获取已实例化的Groovy对象
            //groovy每执行一次脚本，都会生成一个脚本的class对象,如果重复执行同一脚本,则会导致生成多个class对象,频繁触发GC,影响性能
            //所以这里需要缓存已实例化的脚本对象,以脚本内容作为键
            RuleScript script = redisUtils.get(scriptContent);
            if (script == null) {
                // 缓存中不存在该脚本，需要编译并实例化新的Groovy对象
                script = compileAndInstantiateScript(scriptContent);
                // 将脚本对象缓存到Redis，以脚本内容作为键
                redisUtils.set(scriptContent, script);
            }
            // 执行脚本
            script.execute();
            return "执行新的脚本";
        }
    }

    /**
     * 编译并实例化Groovy脚本对象
     */
    @SuppressWarnings("unchecked")
    private RuleScript compileAndInstantiateScript(String scriptContent) throws CompilationFailedException, IOException {
        GroovyClassLoader classLoader = new GroovyClassLoader();
        try (classLoader) {
            Class<RuleScript> scriptClass = (Class<RuleScript>) classLoader.parseClass(scriptContent);
            System.out.println(scriptClass.getName());
            //因为不是同一个类加载器加载的类(GroovyClassLoader和ApplicationClassLoader)，不能直接进行强转,
            //要先获取到类名,再通过类名结合当前线程的类加载器获取类对象,在进行强转
            //直接这样写 return (RuleScript) scriptClass.getDeclaredConstructor().newInstance();
            //会报错java.lang.ClassCastException: class com.hl.groovydemo.scripts.RuleScript cannot be cast to class com.hl.groovydemo.scripts.RuleScript (com.hl.groovydemo.scripts.RuleScript is in unnamed module of loader groovy.lang.GroovyClassLoader$InnerLoader @14ad71be; com.hl.groovydemo.scripts.RuleScript is in unnamed module of loader 'app')
            //即两者不属于同一个类加载器,不能进行强转,所以需要通过类名获取类对象,再进行强转
            Class<?> currentClassLoaderClazz = Class.forName(scriptClass.getName(),
                    true, Thread.currentThread().getContextClassLoader());
            return (RuleScript) currentClassLoaderClazz.getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException e) {
            throw new RuntimeException("无法实例化Groovy脚本", e);
        } catch (ClassNotFoundException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 执行脚本，脚本实例化为类对象,脚本以字符串形式存储在数据库中时使用，使用时直接获取脚本模板字符串，编译成字节码，再实例化，这种方式灵活，
     * 为了避免频繁GC,可以将脚本对象缓存到缓存工具类中，以脚本内容作为键(这里没有使用redis，因为涉及到序列化问题，redis缓存不好处理)
     * 脚本内容如下
     * def execute(String ruleName) {
     * // 动态规则的执行逻辑
     * println(ruleName)
     * }
     */
    @PostMapping("/executeScriptByGroovyObject")
    public String executeScriptByGroovyObject(@RequestBody String scriptContent) throws CompilationFailedException, IOException {
        // 检查groovyScriptCache中是否已存在相同脚本
        if (groovyScriptCache.containsKey(scriptContent)) {
            // 直接从缓存中获取已实例化的Groovy对象并执行
            GroovyObject groovyObject = groovyScriptCache.get(scriptContent);
            // 执行脚本
            groovyObject.invokeMethod("out", new Object[]{"从缓存中获取的Groovy对象测试"});
            //groovyScriptCache.remove(scriptContent);
            return "执行缓存中的脚本";
        } else {
            // 尝试从groovyScriptCache获取已实例化的Groovy对象
            //groovy每执行一次脚本，都会生成一个脚本的class对象,如果重复执行同一脚本,则会导致生成多个class对象,频繁触发GC,影响性能
            //所以这里需要缓存已实例化的脚本对象,以脚本内容作为键
            GroovyObject groovyObject = groovyScriptCache.get(scriptContent);
            if (groovyObject == null) {
                // 缓存中不存在该脚本，需要编译并实例化新的Groovy对象
                groovyObject = getGroovyObject(scriptContent);
                // 将脚本对象缓存到groovyScriptCache
                groovyScriptCache.put(scriptContent, groovyObject);
            }
            // 执行脚本的execute方法,传入参数
            groovyObject.invokeMethod("out", new Object[]{"从外界传入的Groovy对象测试"});
            return "执行新的脚本";
        }
    }

    /**
     * 编译并实例化Groovy脚本对象
     */
    private GroovyObject getGroovyObject(String scriptContent) throws CompilationFailedException, IOException {
        GroovyClassLoader classLoader = new GroovyClassLoader();
        try (classLoader) {
            Class<?> groovyClass = (Class<?>) classLoader.parseClass(scriptContent);
            return (GroovyObject) groovyClass.getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException e) {
            throw new RuntimeException("无法实例化Groovy脚本", e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * javaScript引擎执行脚本
     * js脚本如下
     * function add(a,b){
     * let sum = a+b
     * return sum
     * }
     */
    @PostMapping("/executeScriptByJavaScriptEngine")
    public String executeScriptByJavaScriptEngine(@RequestBody String scriptContent) throws CompilationFailedException {
        //jdk8以后把自带的javaScript引擎nashorn移除了,使用GraalVM的Graal.js代替
        //需要自己在pom.xml中添加相关依赖依赖
        try (Context context = Context.create()) {
            // 在JavaScript引擎中执行简单的JavaScript代码
            Value result = context.eval("js", "1 + 2");
            System.out.println(result.asInt());
            // 调用JavaScript函数
            context.eval("js", scriptContent);
            Value multiplyFunction = context.getBindings("js").getMember("add");
            Value product = multiplyFunction.execute(3, 4);
            System.out.println(product.asInt());
            return String.valueOf(product.asInt());
        } catch (Exception e) {
            log.error("执行JavaScript脚本失败", e);
            return "执行JavaScript脚本失败";
        }
    }
}
